﻿using System;
using System.Collections.Generic;
using System.Windows.Media;
using System.Windows.Input;

using Bling.Core;
using Bling.DSL;
using Bling.WPF;
using Bling.Graphics;
using Bling.DX;
using Bling.Matrices;
using Bling.WPF3D;

namespace Bling.Example {
  public class Advanced : HasHost {
    public Advanced()
      // by passing true to base, an overlay window will be created.
      : base(true) {
      Host.Size = new PointBl(500, 500); // host size must be constant!
      Host.LeftTop = 0d;
      // background of overlay can't be transparent or it won't receive input. It can be a very transparent color though, go fig. 
      OverlayContent.Background = new SolidColorBrushBl() { Color = Colors.Red, Opacity = OverlayContent.IsMouseOver.Condition(.15d, .01d), };
      // create a virtual trackpad to define Rotate.
      QuaternionBl Rotate;
      QuaternionBl RotateOther;
      {
        // An ellipse to represent our virtual trackpad, only visible when the mouse is over the overlay
        var sphere = new EllipseBl(OverlayContent) {
          CenterPosition = OverlayContent.CenterSize,
          Stroke = { Brush = Brushes.White, Thickness = 2d },
          Size = 400d,
          Visibility = OverlayContent.IsMouseOver,
          // so mouse events on the sphere are passed to overlay content instead
          IsHitTestVisible = false,
        };
        // Use two rotations to more accurately represent transient rotation, rotation A is the current rotation
        // while rotation B is the transient rotation. These are anonymous dependency properties. 
        QuaternionBl RotateA = this.Property(new QuaternionBl(1d.Bl().XXX(), .33.PI()));
        QuaternionBl RotateB = this.Property(QuaternionBl.Identity);
        QuaternionBl RotateC = this.Property(QuaternionBl.Identity);

        BoolBl IsShift = BoolBl.State(() => {
          return Keyboard.IsKeyDown(Key.RightCtrl) || Keyboard.IsKeyDown(Key.LeftCtrl);
        });

        // we are row-based, so application occurs from right to left, so rotate b has to occur on the left of the multiply
        // sign, just as in matrix compositions. 
        Rotate = IsShift.Condition(QuaternionBl.Identity,RotateB) *  RotateA;
        RotateOther = (!IsShift).Condition(QuaternionBl.Identity, RotateB) * RotateC;
        Action End;
        {
          Action SetRotateA = QuaternionBl.Assign(RotateA, Rotate);
          Action SetRotateC = QuaternionBl.Assign(RotateC, RotateOther);
          Action ResetRotateB = QuaternionBl.Assign(RotateB, QuaternionBl.Identity);
          // end updates the current rotation to the actual rotation and resets the transient rotation to identity. 
          End = () => { SetRotateA(); SetRotateC();  ResetRotateB(); };
        }
        // compute virtual sphere mouse location according to mouse's 2D position
        PointBl MouseAt = sphere.MousePosition / sphere.Size;
        // screen cooridnates are [0,1] while 3D coordinates are [-1,1], use n * 2d - 1 to translate.
        MouseAt = MouseAt * 2d - 1d;
        // for some reason, X is flipped in result, easy enough to fix. 
        MouseAt = MouseAt * new PointBl(-1d, +1d);
        // determine Z value based on X and Y values, if x,y are outside of the sphere, simply use 0.
        Point3DBl MouseAt3D = new Point3DBl(MouseAt, (1d - MouseAt.LengthSquared).Max(0d).Sqrt);
        // another anonymous dependency property to hold the initial mouse down location.
        Point3DBl MouseInit = this.Property(Point3DBl.Default);
        // initialize the mouse down location
        Action Init = Point3DBl.Assign(MouseInit, MouseAt3D);
        // update transient rotation according to the angle between current mouse location and initial mouse location
        Action Update = QuaternionBl.Assign(RotateB, MouseAt3D.AngleBetween(MouseInit));
        // create a drag handler on overlay content (not sphere, since rotation can happen outside of sphere!
        OverlayContent.InstallDragHandler(() => {
          Init();
          return true;
        }, Update, End);
      }
      // A cube vertex topology, 4 vertices per face.
      CubeTopology Cube = new CubeTopology();

      //  create a perspective camera.
      Point3DBl WorldCenter = new Point3DBl(0, 0, 0);
      // camera position is used in lighting, so externalize
      Point3DBl CameraPosition = WorldCenter + new Point3DBl(0, 0, -4);
      PerspectiveCameraCl Camera = new PerspectiveCameraCl() {
        Position = CameraPosition,
      };
      var DIM = 10;
      Device.Render(Cube.Count, DIM * DIM * DIM, (IntBl n, IntBl m, IVertex vertex) => {
        // n is the vertex index, there are four vertices per face, so divide by 4 to find the face id.
        var face = n / 4;

        // quaternions for both x and y rotation, determined by sliders.
        // m is the instance index, there are 16 instances, arrange in a square configuration using %/ by 4.
        var mxyz = new Point3DBl(m % DIM, (m / DIM) % DIM, (m / DIM) / DIM);
        // the current coord is [0,1,2,3] based, we need to convert to [-1,+1] based.
        mxyz = (mxyz / ((DoubleBl) DIM.Bl()));
        mxyz = mxyz - .4;
        var sz = 2d.Bl() / DIM.Bl();
        sz = sz / 4;

        // find the xyz coordinate of the current vertex index
        Point3DBl WorldPos = Cube[n];
        WorldPos = ((Camera.Project * Camera.View) * 
          Rotate.Matrix * 
          (mxyz).Translate() * sz.XXX().Scale() * RotateOther.Matrix).Transform(WorldPos);
        vertex.Position = WorldPos;

        var FaceColors = new Color[] {
          Colors.Red,
          Colors.LightGreen,
          Colors.Blue,
          Colors.Yellow,
          Colors.LightPink,
          Colors.Salmon,
        };
        ColorBl clr = new TableExpr2<Color>(FaceColors, face % FaceColors.Length);
        // The normal comes from the cube topology, but must be transformed according to the current rotation.
        Point3DBl normal = Rotate.Matrix.Transform(Cube.Normal(face));
        // create a slider to control light height.

        // illuminate the cubes with a point light on a diffuse material.
        LightBl light = new SpotLightBl() {
          Direction = (new Point3DBl(0, 0, +1)).Normalize,
          Color = Colors.White,
          Position = new Point3DBl(0, 0, -10),
          InnerConeAngle = 2.ToDegrees(),
          OuterConeAngle = 4.ToDegrees(),
        };
        MaterialCl diffuse = new DiffuseMaterialCl() { 
          // how much non-ambient light is relfected
          Factor = .2, 
          Ambient = { 
            // how much ambient light is reflected
            Factor = .8 
          } 
        };
        // combine light with diffuse material via apply, plugin in world position, normal, and camera position, then color. 
        // also, compose an ambient light so nothing is completely dark.
        clr = diffuse.
          ApplyLights(light + new AmbientLightBl() { Color = Colors.White }).
          ApplyAt(WorldPos.PerPixel(), normal, CameraPosition).ForColor(clr);
        vertex.Color = clr;
      }, Cube.IndexBuffer);
    }
  }
}
